var entityMap = {
  "&": "&amp;",
  "<": "&lt;",
  ">": "&gt;",
  '"': "&quot;",
  "'": "&#39;",
  "/": "&#x2F;",
  "`": "&#x60;",
  "=": "&#x3D;",
};

function escapeHtml(string) {
  return string.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
}

function hasProperty(obj, propName) {
  return obj != null && typeof obj === "object" && propName in obj;
}

function escapeRegExp(string) {
  return string.replace(/[-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}

function parseTemplate(template) {
  const tokens = [];
  let value;
  const scanner = new Scanner(template);
  let type;
  let temp = 0;
  let start = 0;
  // 单行是否有非空格的其他内容
  let nonSpace = false;
  // 单行是否有其他标签
  let hasTag = false;
  // 保存空格所在位置的数组
  let space = [];
  let lineHasNonSpace = false;
  let tagIndex = 0;
  let indendation = "";

  function stripSpace() {
    if (hasTag && !nonSpace) {
      while (space.length) {
        delete tokens[space.pop()];
      }
    } else {
      space = [];
    }
    hasTag = false;
    nonSpace = false;
  }

  // 解析template
  let openingTagRe;
  let closingTagRe;
  let closingCurlyRe;
  // 解析template
  function compileTags(tagsToCompile) {
    if (typeof tagsToCompile === "string") {
      tagsToCompile = tagsToCompile.split(/\s+/, 2);
    }
    if (!Array.isArray(tagsToCompile) || tagsToCompile.length !== 2) {
      throw new Error("不合法的标签：" + tagsToCompile);
    }
    openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + "\\s*");
    closingTagRe = new RegExp("\\s*" + escapeRegExp(tagsToCompile[1]));
    closingCurlyRe = new RegExp("\\s*" + escapeRegExp("}" + tagsToCompile[1]));
  }

  compileTags("{{ }}");

  while (!scanner.eos() && temp < 1000) {
    temp++;
    start = scanner.pos;
    // 找到{{之前的文本
    value = scanner.scanUntil(openingTagRe);
    if (value) {
      for (const chr of value) {
        if (/\s/.test(chr)) {
          space.push(tokens.length);
          indendation += chr;
        } else {
          // 表示此处有非空字符
          nonSpace = true;
          lineHasNonSpace = true;
          indendation += " ";
        }
        tokens.push(["text", chr, start, ++start]);
        if (chr === "\n") {
          // 清理空格
          stripSpace();
          lineHasNonSpace = false;
          tagIndex = 0;
          indendation = "";
        }
      }
    }
    // 跳过{{，如果没有跳过的内容，则表示已经结束了
    if (!scanner.scan(openingTagRe)) {
      break;
    }
    // 能够跳过{{，说明此处有标签
    hasTag = true;
    // 获取标签类型
    type = scanner.scan(/\{|#|\/|\^|\!|\>|=/) || "name";
    // 跳过{{后的空格
    scanner.scan(/\s*/);
    if (type === "=") {
      value = scanner.scanUntil(/\s*=/);
      scanner.scan(/\s*=/);
      scanner.scanUntil(closingTagRe);
    } else if (type === "{") {
      value = scanner.scanUntil(closingCurlyRe);
      scanner.scan(/\}/);
      scanner.scanUntil(closingTagRe);
    } else {
      // 找到}}之前的内容
      value = scanner.scanUntil(closingTagRe);
    }
    // 跳过}}
    scanner.scan(closingTagRe);
    if (type === ">") {
      tokens.push([
        type,
        value,
        start,
        scanner.pos,
        indendation,
        tagIndex,
        lineHasNonSpace,
      ]);
    } else {
      tokens.push([type, value, start, scanner.pos]);
    }
    tagIndex++;
    if (type === "=") {
      compileTags(value);
    }
  }
  stripSpace();
  return nestTokens(squashTokens(tokens));
}

function squashTokens(tokens) {
  const squashedTokens = [];
  let lastToken;
  for (const token of tokens) {
    if (!token) {
      continue;
    }
    if (token[0] === "text" && lastToken && lastToken[0] === "text") {
      lastToken[1] += token[1];
      lastToken[3] = token[3];
    } else {
      lastToken = token;
      squashedTokens.push(lastToken);
    }
  }
  return squashedTokens;
}

function nestTokens(tokens) {
  const nestedTokens = [];
  let collector = nestedTokens;
  const sections = [];
  let section;
  for (const token of tokens) {
    switch (token[0]) {
      case "#":
      case "^":
        collector.push(token);
        sections.push(token);
        collector = token[4] = [];
        break;
      case "/":
        section = sections.pop();
        section[5] = token[2];
        collector = sections.length
          ? sections[sections.length - 1][4]
          : nestedTokens;
        break;
      default:
        collector.push(token);
    }
  }
  return nestedTokens;
}

class Context {
  constructor(view, parentContext) {
    this.view = view;
    this.parent = parentContext;
  }
  push(value) {
    return new Context(value, this);
  }
  lookup(name) {
    let context = this;
    let value;
    let intermediateValue;
    let lookupHit;
    while (context) {
      intermediateValue = context.view[name];
      lookupHit = hasProperty(context.view, name);
      if (lookupHit) {
        value = intermediateValue;
        break;
      }
      context = context.parent;
    }

    if (typeof value === "function") {
      value = value.call(this.view);
    }
    return value;
  }
}

class Scanner {
  constructor(string) {
    this.string = string;
    this.tail = string;
    this.pos = 0;
  }
  eos() {
    return this.tail === "";
  }
  // 跳过reg匹配的内容
  scan(reg) {
    let matchedValue;
    const match = this.tail.match(reg);
    if (Array.isArray(match) && match.index === 0) {
      matchedValue = match[0];
      this.tail = this.tail.slice(matchedValue.length);
      this.pos += matchedValue.length;
    }
    return matchedValue;
  }
  // 跳到reg匹配的内容
  scanUntil(reg) {
    const index = this.tail.search(reg);
    let matchedValue;
    if (index > -1) {
      matchedValue = this.tail.slice(0, index);
      this.tail = this.tail.slice(index);
      this.pos += index;
    } else {
      matchedValue = this.tail;
      this.tail = "";
      this.pos = this.string.length;
    }
    return matchedValue;
  }
}

class Writer {
  render(template, view, partials) {
    const tokens = this.parse(template);
    console.log(tokens);
    const context = new Context(view);
    return this.renderTokens(tokens, context, partials, template);
  }
  parse(template) {
    return parseTemplate(template);
  }
  renderTokens(tokens, context, partials, template) {
    let buffer = "";
    let type;
    let value;
    for (let token of tokens) {
      value = "";
      type = token[0];
      switch (type) {
        case "text":
          value = this.rawValue(token);
          break;
        case "name":
          value = this.escapedValue(token, context);
          break;
        case "{":
          value = context.lookup(token[1]);
          break;
        case "#":
          value = this.renderSection(token, context, partials, template);
          break;
        case "^":
          value = this.renderInverted(token, context, partials, template);
          break;
        case ">":
          value = this.renderPartial(token, context, partials);
      }
      if (value != null) {
        buffer += value;
      }
    }
    return buffer;
  }
  rawValue(token) {
    return token[1];
  }
  escapedValue(token, context) {
    let value = context.lookup(token[1]);
    if (value != null) {
      value = typeof value === "number" ? String(value) : escapeHtml(value);
    }
    return value;
  }
  renderSection(token, context, partials, originalTemplate) {
    let buffer = "";
    let value = context.lookup(token[1]);
    const self = this;
    const subRender = function (template) {
      return self.render(template, context, partials);
    };
    if (!value) {
      return;
    }
    if (Array.isArray(value)) {
      for (const item of value) {
        buffer += this.renderTokens(
          token[4],
          context.push(item),
          partials,
          originalTemplate
        );
      }
    } else if (typeof value === "function") {
      buffer = value.call(
        context.view,
        originalTemplate.substring(token[3], token[5]),
        subRender
      );
    } else if (typeof value === "object") {
      buffer = this.renderTokens(
        token[4],
        context.push(value),
        partials,
        originalTemplate
      );
    } else {
      buffer = this.renderTokens(token[4], context, partials, originalTemplate);
    }
    return buffer;
  }
  renderInverted(token, context, partials, originalTemplate) {
    const value = context.lookup[token[1]];

    if (!value || (Array.isArray(value) && value.length === 0)) {
      return this.renderTokens(token[4], context, partials, originalTemplate);
    }
  }
  indentPartial(partial, indendation, lineHasNonSpace) {
    const filteredIndentation = indendation.replace(/[^ \t]/g, "");
    const partialByN1 = partial.split("\n");
    for (let i = 0; i < partialByN1.length; i++) {
      if (partialByN1[i].length && (i > 0 || !lineHasNonSpace)) {
        partialByN1[i] = filteredIndentation + partialByN1[i];
      }
    }
    return partialByN1.join("\n");
  }
  renderPartial(token, context, partials) {
    if (!partials) {
      return;
    }
    var value =
      typeof partials === "function" ? partials(token[1]) : partials[token[1]];
    if (value != null) {
      const indentation = token[4];
      const tagIndex = token[5];
      const lineHasNonSpace = token[6];
      let indentedValue = value;
      if (tagIndex == 0 && indentation) {
        indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
      }
      const tokens = this.parse(indentedValue);
      return this.renderTokens(tokens, context, partials, indentedValue);
    }
  }
}

const defaultWriter = new Writer();

const Mustache = {
  render(template, view, partials) {
    document.getElementById("template").textContent = template;
    return defaultWriter.render(template, view, partials);
  },
};

export default Mustache;
